﻿using GNUGo.Net.WebApp.WsClasses;
using NoNameGoServer.Client;
using NoNameGoServer.Client.Commands;
using System;
using System.Linq;
using GNUGo.Net.WebApp.Entities;
using System.Threading;
using System.Text;

namespace GNUGo.Net.WebApp {
	public class InternetGoServer {
		private const string IGS_IP = "210.155.158.200";
		private const int IGS_PORT = 7777;
		private const int THREAD_SLEEP = 500;
		
		public WsResult<object> CanLogIn(string username, string password) {
			using (NNGSClient c = new NNGSClient()) {
				try {
					bool timeoutDidNotExpire = c.Connect(IGS_IP, IGS_PORT, username, password, new TimeSpan(0, 0, 0, 20));

					if (timeoutDidNotExpire)
						return new WsResult<object>();

					return new WsResult<object> { Success = false, ErrorMessage = "Timeout Expired" };
				} catch (MultiException ex) {
					return new WsResult<object> { Success = false, ErrorMessage = ex.Message };
				} catch (NNGSClientException ex) {
					return new WsResult<object> { Success = false, ErrorMessage = ex.Message };
				} catch (Exception ex) {
					return new WsResult<object> { Success = false, ErrorMessage = "Unexpected Exception: " + ex };
				}
			}
		}

		public void GetIgsRank(
			string username, 
			string password, 
			out string rank, 
			out int rating, 
			out bool isRated
			) {

			using (NNGSClient c = new NNGSClient()) {
				c.Connect(IGS_IP, IGS_PORT, username, password);

				StatsCommand cmd = new StatsCommand();
				c.EnqueueCommandAndWait(cmd);

				// rating = 13d*   14012
				// rank = 13d
				// IsRated = *
				// rating = 14012

				rank = cmd.Results.Rank;
				isRated = cmd.Results.IsRated;
				Int32.TryParse(cmd.Results.Rating, out rating);
			}
		}

		private void ConnectToIgs(NNGSClient client, GoPlayer player) {
			string username = player.IgsUsername;
			string password = EncDec.Decrypt(player.IgsPassword, GoServices.ENCDECPWD, player.PlayerId);
			client.Connect(IGS_IP, IGS_PORT, username, password);

			SendCommand(client, new SetVariableCommand("open", "on"));
			SendCommand(client, new SetVariableCommand("looking", "off"));
		}

		private GoMove[] _allMoves;
		private int _moveNumber;
		private int _allMovesIterator;
		private int _totalMoves;
		private bool _blackToMove;
		private NNGSClient _cWhite;
		private NNGSClient _cBlack;
		private int _boardSize;
		private GoGame _game;
		private const string LETTERS = "ABCDEFGHJKLMNOPQRSTUVWXYZ";
		private readonly EventWaitHandle _operationComplete = new EventWaitHandle(false, EventResetMode.ManualReset);
		private Exception _exception;
		private int _finalPassesSent = 0;

		public StringBuilder DebugLog { get; set; }
		public decimal FinalScoreWhite { get; set; }
		public decimal FinalScoreBlack { get; set; }

		public void PlayGoGame(
			int gameId
			) {

			// connect to the db, get relevant information
			GoEntitiesDataContext context = GoServices.GetDataContext();
			_game = context.GoGames.SingleOrDefault(g => g.GameId == gameId);
			GoPlayer playerWhite = _game.GoPlayerWhite;
			GoPlayer playerBlack = _game.GoPlayerBlack;

			_boardSize = _game.BoardSize;
			_allMoves = _game.GoMoves.OrderBy(m => m.MoveNumber).ToArray();
			_totalMoves = _allMoves.Length;
			if (_totalMoves == 0)
				return;

			// black always starts if there is a 0 handicap.  Otheriwse white starts.
			_blackToMove = _game.BlackHandicap == 0;

			bool timeoutOccurred;

			using (_cWhite = new NNGSClient()) {
				ConnectToIgs(_cWhite, playerWhite);

				_cWhite.DataReceived += new NNGSClient.DataReceivedDelegate(cWhite_DataReceived);
				_cWhite.ScoreReceived += new NNGSClient.ScoreReceivedDelegate(NNGSClient_ScoreReceived);
				_cWhite.MoveMadeReceived += new NNGSClient.MoveMadeReceivedDelegate(cWhite_MoveMadeReceived);
				_cWhite.PassReceived += new NNGSClient.PassReceivedDelegate(cWhite_PassReceived);

				using (_cBlack = new NNGSClient()) {
					_cBlack.DataReceived += new NNGSClient.DataReceivedDelegate(cBlack_DataReceived);
					_cBlack.MoveMadeReceived += new NNGSClient.MoveMadeReceivedDelegate(cBlack_MoveMadeReceived);
					_cBlack.PassReceived += new NNGSClient.PassReceivedDelegate(cBlack_PassReceived);
					
					ConnectToIgs(_cBlack, playerBlack);

					// white sends a match request to black
					SendMatchRequest(playerBlack, _cWhite, StoneColor.White);

					// black re-sends the match request as-is to accept
					SendMatchRequest(playerWhite, _cBlack, StoneColor.Black);

					if (_game.BlackHandicap != 0) {
						// black sends the handicap request if there is one
						SendHandicap(_game, _cBlack);
					}

					SendKomi(_game.Komi, _cBlack);
					SendKomi(_game.Komi, _cWhite);

					// we checked earlier that total moves is >= 1
					GoMove firstMove = _allMoves[0];

					// asynchronous move processing will begin at the 2nd move (_currentMove = 1) since we're playing one now
					_moveNumber = 1;

					// if the first move is a pass
					if (firstMove.MoveNumber != 0) {
						_allMovesIterator = 0;
						SendCommand(_cBlack, _cWhite, new PassCommand());
					} else {
						_allMovesIterator = 1;
						PlayCommand cmd = GetPlayCommand(firstMove.X, firstMove.Y, _boardSize);
						SendCommand(_cBlack, _cWhite, cmd);
					}

					// wait until a score is received, an exception is received,
					//		or 2 minutes, whichever is less
					timeoutOccurred = !_operationComplete.WaitOne(2*60*1000, false);
				}
			}

			if (timeoutOccurred)
				throw new Exception("Timeout occurred during IGS session.");

			// if an exception occurred during this operation throw it now
			if (_exception != null)
				throw _exception;
		}

		void cBlack_PassReceived(NNGSClient sender, PassReceivedData passReceivedData) {
			if (_blackToMove)
				RespondToPassOrMove(StoneColor.Black, passReceivedData.Color);
		}

		void cBlack_MoveMadeReceived(NNGSClient sender, MoveMadeData moveMadeData) {
			if (_blackToMove)
				RespondToPassOrMove(StoneColor.Black, moveMadeData.Color);
		}

		void cWhite_PassReceived(NNGSClient sender, PassReceivedData passReceivedData) {
			if (!_blackToMove)
				RespondToPassOrMove(StoneColor.White, passReceivedData.Color);
		}

		void cWhite_MoveMadeReceived(NNGSClient sender, MoveMadeData moveMadeData) {
			if (!_blackToMove)
				RespondToPassOrMove(StoneColor.White, moveMadeData.Color);
		}

		private void RespondToPassOrMove(StoneColor activePlayer, StoneColor whoJustPlayed) {
			// don't listen to your own events
			if (whoJustPlayed == activePlayer)
				return;

			// if there are remaining unprocessed moves
			if (_allMovesIterator < _totalMoves)
				PlayNextMove(activePlayer);
			else
				FinalizeGame(activePlayer);
		}

		private void FinalizeGame(StoneColor activePlayer) {
			// wait for other plays to complete
			Thread.Sleep(THREAD_SLEEP);

			// send three passes to finish the game
			if (_finalPassesSent < 3) {
				WriteDebug("final pass", false, activePlayer);
				_finalPassesSent++;
				SendCommand(_cBlack, _cWhite, new PassCommand());
				if (_finalPassesSent == 1)
					return;
			} else {
				// ignore all commands after this point
				return;
			}

			// wait for the other conversation to pass
			Thread.Sleep(THREAD_SLEEP);

			if (activePlayer == StoneColor.Black) {
				// identify dead stones
				foreach (GoDeadGroup deadGroup in _game.GoDeadGroups) {
					Command deadCmd = GetPlayCommand(deadGroup.X, deadGroup.Y, _boardSize);
					WriteDebug("markdead " + deadCmd, false, activePlayer);
					SendCommand(_cBlack, deadCmd);
				}
				WriteDebug("done", false, activePlayer);
				SendCommand(_cBlack, new DoneCommand());
			} else {
				Thread.Sleep(THREAD_SLEEP);
			}

			WriteDebug("done", false, activePlayer);
			SendCommand(_cWhite, new DoneCommand());
			// goto NNGSClient_ScoreReceived
		}

		private void PlayNextMove(StoneColor activePlayer) {
			GoMove nextMove = _allMoves[_allMovesIterator];

			WriteDebug("PlayNextMove() MoveId = " + nextMove.MoveId + " _moveNumber = " + _moveNumber + " _allMovesIterator = " + _allMovesIterator, false, activePlayer);

			// determine if there was a pass by looking at the move's move number
			//		(assuming the moves are sorted ascending)
			bool currentMoveIsPass = _moveNumber < nextMove.MoveNumber;

			_moveNumber++;

			// if the first move is a pass
			if (currentMoveIsPass) {
				SendCommand(_cBlack, _cWhite, new PassCommand());
			} else {
				_allMovesIterator++;
				PlayCommand cmd = GetPlayCommand(nextMove.X, nextMove.Y, _boardSize);
				SendCommand(_cBlack, _cWhite, cmd);
			}
		}

		void cBlack_DataReceived(NNGSClient source, NNGSClient.DataReceivedData args) {
			string line = Encoding.ASCII.GetString(args.Buffer, 0, args.Count);
			WriteDebug(line, true, StoneColor.Black);
		}

		private void WriteDebug(string line, bool receive, StoneColor stoneColor) {
			string line1 = String.Format("-------- {0} {1:mm-ss} {2} --------",
				receive ? "RECEIVE" : "SEND",
				DateTime.Now,
				stoneColor == StoneColor.Black ? "BLACK" : "WHITE");

			if (DebugLog == null) {
				Console.WriteLine(line1);
				Console.WriteLine(line);
			} else {
				DebugLog.AppendLine(line1);
				DebugLog.AppendLine(line);
			}
		}

		void cWhite_DataReceived(NNGSClient source, NNGSClient.DataReceivedData args) {
			string line = Encoding.ASCII.GetString(args.Buffer, 0, args.Count);
			WriteDebug(line, true, StoneColor.White);
		}

		private void SendKomi(decimal komi, NNGSClient client) {
			SendCommand(client, new SetKomiCommand(komi));
			Thread.Sleep(THREAD_SLEEP);
		}

		private static PlayCommand GetPlayCommand(int X, int Y, int boardSize) {
			int oldX = X;
			int oldY = Y;

			X = oldY;
			Y = boardSize - oldX - 1;

			char nngsLetter = LETTERS[X];
			int nngsNumber = Y + 1;

			return new PlayCommand(nngsLetter, nngsNumber);
		}

		void NNGSClient_ScoreReceived(NNGSClient sender, ScoreData scoreData) {
			FinalScoreBlack = scoreData.BlackScore;
			FinalScoreWhite = scoreData.WhiteScore;
			_operationComplete.Set();
			// aka allow the synchronous PlayGoGame() method to return
		}

		private void SendHandicap(GoGame game, NNGSClient cWhite) {
			SendCommand(cWhite, new HandicapCommand(game.BlackHandicap));
			Thread.Sleep(THREAD_SLEEP);
		}

		private void SendMatchRequest(GoPlayer player, NNGSClient client, StoneColor color) {
			MatchRequestCommand request = new MatchRequestCommand(
				player.IgsUsername,
				color,
				_boardSize,
				1,
				1);

			SendCommand(client, request);
			
			// if two connections are open simultaneously, then a short sleep is necessary
			//		to wait for IGS to asynchronously send the command to the other user
			Thread.Sleep(THREAD_SLEEP);
		}

		private void SendCommand(NNGSClient client, Command cmd) {
			try {
				// this is where any exceptions will be thrown if there are any
				client.EnqueueCommand(cmd);
			} catch (Exception ex) {
				// capture the exception to re-throw in PlayGoGame()
				_exception = ex;
				// set the flag to stop waiting at the end of PlayGoGame();
				_operationComplete.Set();
			}
		}

		private void SendCommand(NNGSClient cBlack, NNGSClient cWhite, Command cmd) {
			if (_blackToMove) {
				WriteDebug(cmd.ToString(), false, StoneColor.Black);
				_blackToMove = false;
				SendCommand(cBlack, cmd);
			} else {
				WriteDebug(cmd.ToString(), false, StoneColor.White);
				_blackToMove = true;
				SendCommand(cWhite, cmd);
			}
		}
	}
}
